--[[ Copyright 2012, Chris Ho                               ]]--
--[[ MIT License (Please read the LICENSE file for details) ]]--

-- Crinkle's Persona
-- Conventions: always end statements with semicolon
-- Global variables are all uppercase
-- Functions are CamelCase, with first letter capitalized
--
-- To use this as a Homunculus AI, put it in USER_AI and type "/hoai"
-- To use this as a Mercenary AI, put it in USER_AI and type "/merai"
-- To enable file logging, type "/traceai" (to "TraceAI.txt" in iRO install path)

-- -- -- -- -- -- -- --

Use("common/Const");
Use("common/Log");
Use("common/List");
Use("common/Position");
Use("common/Task");
Use("common/Pet");
Use("common/Clique");
Use("tasks/Kill");
Use("tasks/Charge");
Use("tasks/Bombard");

-- Core Version
CORE_VERSION = "Crinkle's Persona (Core v2.5)";

-- Maximum Number of Queued Commands
CAPACITY = 10;

-- Desired Logging Visibility (irrelevant if /traceai is off)
LOG_LEVEL = LOG_INFO;
--LOG_LEVEL = LOG_DEBUG;

-- Default Personality Files
PERSONALITY_DEFAULT = PATH .. "Persona.lua";

-- Command Execution Phases (3 Main Sequences)
-- Plan -> Command -> Automatic -> Busy -> Idle
-- Plan -> Command -> Busy -> Idle
-- Plan -> Automatic -> Busy -> Idle
PHASE_PLAN = 0;
PHASE_AUTOMATIC = 1;
PHASE_COMMAND = 2;
PHASE_BUSY = 3;
PHASE_IDLE = 4;

-- -- -- -- -- -- -- --

-- Pet Instance
ME = nil;

-- List of User Command Records
QUEUE = List.new();

-- Request for Tasks
PHASE = PHASE_IDLE;

-- Actual Task (May Differ from Pet Task!)
-- The pet has its own task, but this task takes precedence.
TASK = {};

-- Current list of actors:
ACTORS = {};

-- -- -- -- -- -- -- --

--[[ Process Commands. Avoid modifying the pet's current task! ]]--
-- Simply convert the command into our task system.
--
-- MOVE_CMD changes to program (stopped) mode (if moved to same position)
-- STOP_CMD changes pet mode.
-- PATROL_CMD changes pet mode.
-- HOLD_CMD changes pet mode (defend).
-- FOLLOW_CMD changes pet mode (standard escort mode).
function Execute(record)
  Debug("Execute() " .. record[1]);
  if (record ~= nil) then
    if (record[1] == MOVE_CMD) then OnMOVE(record)
    elseif (record[1] == STOP_CMD) then OnSTOP(record)
    elseif (record[1]== ATTACK_OBJECT_CMD) then OnATTACK_OBJECT(record)
    elseif (record[1] == ATTACK_AREA_CMD) then OnATTACK_AREA(record)
    elseif (record[1] == PATROL_CMD) then OnPATROL(record)
    elseif (record[1] == HOLD_CMD) then OnHOLD(record)
    elseif (record[1] == SKILL_OBJECT_CMD) then OnSKILL_OBJECT(record)
    elseif (record[1] == SKILL_AREA_CMD) then OnSKILL_AREA(record)
    elseif (record[1] == FOLLOW_CMD) then OnFOLLOW(record)
    else
      Alert("Execute() ignoring request: " .. record[1])
      PHASE = PHASE_IDLE;
      TASK = Task.Create(ME);
    end
  end
end

--[[ Handle MOVE_CMD ]]--
function OnMOVE(record)
  Info("OnMOVE()");
  -- [2] and [3] are the x and y coordinates.
  local from = Position.Find(ME.id);
  local to = Position.Create(record[2], record[3]);

  if ((ME.mode ~= MODE_STOP) and (to.x == from.x) and (to.y == from.y)) then
    -- Go into STOP mode. In STOP mode, players can do some in-game
    -- reprogramming of the pet. When going into STOP mode, we
    -- autmatically clear out the existing friend list.
    ME:Behave(MODE_STOP);
    ME.friends = {};
    PHASE = PHASE_AUTOMATIC;
  else
    if (ME.mode == MODE_STOP) then
      -- Make Friends:
      ME:FindFriends(ACTORS, to)
    end

    -- Move normally.
    TASK = Go.Create(ME, Position.Create(record[2], record[3]));
    PHASE = PHASE_BUSY;
  end
end

--[[ Handle STOP_CMD (switch pet mode to MODE_STOP) ]]--
function OnSTOP(record)
  Info("OnSTOP()");

  -- Just switch to the stop mode. Nothing happens.
  ME:Behave(MODE_STOP);
  ME.task = Task.Create(ME);

  -- Use the pet task.
  PHASE = PHASE_AUTOMATIC;
end

--[[ Handle ATTACK_OBJECT_CMD ]]--
function OnATTACK_OBJECT(record)
  Info("OnATTACK_OBJECT()");

  -- [2] is the target ID
  TASK = Kill.Create(ME, record[2]);
  PHASE = PHASE_BUSY;
end

--[[ Handle ATTACK_AREA_CMD]]--
function OnATTACK_AREA(record)
  Info("OnATTACK_AREA()");

  -- [2] and [3] are the x and y coordinates.
  TASK = Bombard.Create(ME, Position.Create(record[2], record[3]));
  PHASE = PHASE_BUSY;
end

--[[ Handle PATROL_CMD(switch pet mode to MODE_PATROL) ]]--
function OnPATROL(record)
  Info("OnPATROL()");

  -- Switch to the patrol mode. [2] and [3] are the x and y coordinates.
  ME:Behave(MODE_PATROL);
  ME.destination = Position.Create(record[2], record[3]);
  ME.task = Patrol.Create(ME, Position.Find(ME.id), ME.destination);

  -- Use the pet task.
  PHASE = PHASE_AUTOMATIC;
end

--[[ Handle HOLD_CMD(switch pet mode to MODE_DEFEND) ]]--
function OnHOLD(record)
  Info("OnHOLD()");

  -- When holding, we stop but can still act.
  ME:Behave(MODE_DEFEND);
  ME.task = Task.Create(ME);

  -- Let the pet automatically respond.
  PHASE = PHASE_AUTOMATIC;
end

--[[ Handle SKILL_OBJECT_CMD ]]--
function OnSKILL_OBJECT(record)
  Info("OnSKILL_OBJECT() (Skill " .. record[3] .. ", level " .. record[2] .. ")");

  -- [2] is level, [3] is skill, [4] is target ID
  TASK = Charge.Create(ME, Skill.Create(ME.id, record[3], record[2]), record[4]);
  PHASE = PHASE_BUSY;
end

--[[ Handle SKILL_AREA_CMD ]]--
function OnSKILL_AREA(record)
  Info("OnSKILL_AREA() (Skill " .. record[3] .. ", level " .. record[2] .. ")");

  -- [2] is level, [3] is skill, [4] is x, and [5] is y.
  TASK = Bombard.Create(ME, Skill.Create(ME.id, record[3], record[2]), Position.Create(record[4], record[5]));
  PHASE = PHASE_BUSY;
end

--[[ Handle FOLLOW_CMD (switch pet mode to MODE_ESCORT) ]]--
function OnFOLLOW(record)
  Debug("OnFOLLOW()");

  -- If we were in stop (program) mode, then save the friends list
  -- before we start escorting.
  if (ME.mode == MODE_STOP) then Clique.Save(FRIENDS_FILE, ME.friends) end

  if (ME.mode == MODE_ESCORT) then
    -- Force the escort state.
    ME:Behave(MODE_ESCORT);
    ME.leader = ME.owner.id;

    -- We were already in escort mode. Assist the owner.
    ME:Help(ME.leader);

    -- Execute the result of the help request.
    TASK = ME.task;
    PHASE = PHASE_BUSY;
  else
    -- We are changing to the owner-escort state.
    ME:Behave(MODE_ESCORT);
    ME.leader = ME.owner.id;

    -- Let the pet decide its task.
    PHASE = PHASE_AUTOMATIC;
  end
end

-- -- -- -- -- -- -- --

--[[ Initialize the pet global ]]--
-- You must provide the pet ID and the persona file name.
function Initialize(_id, _filename)
  -- Create the global pet instance.
  ME = Pet.Create(_id)

  -- Load the personality file.
  local Personify, status = loadfile(_filename);
  if (Personify == nil) then
    Alert("Initialize() couldn't load " .. _filename .." (" .. status .. "), trying default.");
    Personify = loadfile(PERSONALITY_DEFAULT);
  end

  -- Install the personality.
  Personify(ME);

  -- Report some initialization notes.
  Info("Initialize() " .. CORE_VERSION .." on '" .. _VERSION .. "' Initilizing (LEASH=" .. LEASH .. ", VISIBILITY=" .. VISIBILITY .. ", CAPACITY=" .. CAPACITY .. ", TRANQUILITY=" .. TRANQUILITY .. ", RESILIENCE=" .. RESILIENCE .. ")");

  -- Initialize AI pipeline.
  PHASE = PHASE_PLAN;
  TASK = ME.task;
end

--[[ Gather Information (Read Input and Environment) ]]--
function Survey()
  -- Interpret incoming messages.
  local message = GetMsg(ME.id);
  local reserve = GetResMsg(ME.id);

  if (message[1] == NONE_CMD) then
    -- Import any reserve commands.
    if (reserve[1] ~= NONE_CMD) then
      if (List.size(QUEUE) < CAPACITY) then
        List.pushright(QUEUE, reserve);
      end
    end
  else
    -- When we get a new top level command, reset the queue.
    List.clear(QUEUE);
    List.pushright(QUEUE, message);

    -- Force the planning state to figure out new tasks.
    -- Unless we come up with an actual task, do nothing.
    TASK = Task.Create(ME);
    PHASE = PHASE_PLAN;
    Info("New Command: " .. message[1]);
  end

  -- Cache the current list of actors.
  ACTORS = GetActors();
end

--[[ Decide on a course of action ]]--
function Decide()
  -- Decide whether to use automatic behavior or player command behavior.
  if (PHASE == PHASE_PLAN) then
    if (List.size(QUEUE) == 0) then
      -- Automatic commands come from the pet's current mode.
      PHASE = PHASE_AUTOMATIC;
    else
      -- Switch to command execution state.
      PHASE = PHASE_COMMAND;
    end
  end

  -- If we are going to process a command, do it first. The command may put
  -- us in an automatic phase.
  if (PHASE == PHASE_COMMAND) then
    -- Execute the next message directly.
    Execute(List.popleft(QUEUE));
  end

  -- Process automatic behavior (from pet mode) if necessary.
  if (PHASE == PHASE_AUTOMATIC) then
    -- Figure out the current task.
    ME:Think();

    -- We're now executing the current task from the pet mode.
    TASK = ME.task;
    PHASE = PHASE_BUSY;
  end
end

--[[ Process the current task ]]--
function Act()
  -- If we have a pending/active task, then process it.
  if ((TASK.progress == PROGRESS_NEW) or (TASK.progress == PROGRESS_ACTIVE)) then
    -- Process Pet Tasks.
    TASK:Execute()

    -- If we finished the task, go idle.
    if (TASK.progress == PROGRESS_SUCCESS) then
      Debug("Act() Task Complete");
      PHASE = PHASE_IDLE;
    elseif (TASK.progress == PROGRESS_FAILURE) then
      Alert("Act() Task FAILED");
      PHASE = PHASE_IDLE;
    end
  else
    -- All tasks complete?
    PHASE = PHASE_IDLE;
  end
end

--[[ Current cycle completes, review and react to the situation ]]--
function Reflect()
  -- Automatically respond to the current environment.
  ME:React();
end
